﻿using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using PhiStore.Communication;
using PhiStore.ObjectManaging;
using PhiStore.Packages;
using PhiStore.Security;
using PhiStore.Storage;
using PhiStore.Threading;

namespace PhiStore
{
    internal class PhiServer
    {
        private object mutex = new object();

        private BroadcastObject bc;

        private Channel<int> portmodifier;
        private Channel<BroadcastObject> toBroadcaster;
        private Channel<PhiInternalPackage> toObjectManager;

        private bool broadcasting = false;

        private int broadcastport = 24281;
        private int broadcastinterval = 2;

        private static PhiServer instance = null;

        private bool running = false;

        public bool Running
        {
            get
            {
                lock (mutex)
                {
                    return running;
                }
            }
        }

        private PhiServer()
        {
            bc = new BroadcastObject(EncryptionSettings.Instance.PublicKey, myIP(), 24282);
        }

        public static PhiServer Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new PhiServer();
                }
                return instance;
            }
        }

        public void Start()
        {
            lock (mutex)
            {
                if (!running && StorageManager.Instance.ReadyToRun &&
                    ReceiveBroadcast.isPortAvailable(broadcastport) &&
                    ReceiveBroadcast.isPortAvailable(bc.Port))
                {
                    Initialize();
                    doStart();
                }
            }
        }

        public BroadcastObject BroadcastObject
        {
            get
            {
                BroadcastObject result;
                lock (mutex)
                {
                    result = bc.Copy();
                }
                return result;
            }
        }

        public int Port
        {
            get
            {
                lock (mutex)
                {
                    return bc.Port;
                }
            }
            set
            {
                lock (mutex)
                {
                    bc = new BroadcastObject(bc.PublicKey, myIP(), value);

                    if (running)
                    {
                        toBroadcaster.Send(bc.Copy());
                        portmodifier.Send(value);
                    }
                }
            }
        }

        public int BroadcastPort
        {
            get
            {
                lock (mutex)
                {
                    return broadcastport;
                }
            }
            set
            {
                lock (mutex)
                {
                    if (broadcastport != value)
                    {
                        broadcastport = value;

                        if (broadcasting && running)
                        {
                            toBroadcaster.Poison();
                            toBroadcaster = new Channel<BroadcastObject>(5);
                            Booster.Boost(new Broadcaster(toBroadcaster, broadcastport, broadcastinterval));
                            toBroadcaster.Send(bc.Copy());
                        }

                    }
                }
            }
        }

        public int BroadcastInterval
        {
            get
            {
                lock (mutex)
                {
                    return broadcastinterval;
                }
            }
            set
            {
                lock (mutex)
                {
                    if (broadcastinterval != value)
                    {
                        broadcastinterval = value;

                        if (broadcasting && running)
                        {
                            toBroadcaster.Poison();
                            toBroadcaster = new Channel<BroadcastObject>(5);
                            Booster.Boost(new Broadcaster(toBroadcaster, broadcastport, broadcastinterval));
                            toBroadcaster.Send(bc.Copy());
                        }
                    }
                }
            }

        }

        public bool Broadcast
        {
            get
            {
                lock (mutex)
                {
                    return broadcasting;
                }
            }
            set
            {
                lock (mutex)
                {
                    if (broadcasting && !value && running)
                    {
                        toBroadcaster.Poison();
                        toBroadcaster = new Channel<BroadcastObject>(5);
                    }

                    broadcasting = value;
                    if (broadcasting && running)
                    {
                        Booster.Boost(new Broadcaster(toBroadcaster, broadcastport, broadcastinterval));
                        toBroadcaster.Send(bc.Copy());
                    }

                }
            }
        }

        public bool isSynchronized { get { return true; } }

        public void Stop()
        {
            if (running)
            {
                portmodifier.Poison();
                toBroadcaster.Poison();
                toObjectManager.Poison();

                foreach (PeerContainer pc in PeerManager.Instance.GetPeers())
                {
                    pc.PeerWorkerChannel.Poison();
                }

                PeerManager.Instance.Clear();

                running = false;
            }
        }

        private IPAddress localIP = null;
        private IPAddress myIP()
        {
            lock (mutex)
            {
                if (localIP == null)
                {
                    IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
                    foreach (IPAddress ip in host.AddressList)
                    {
                        if (ip.AddressFamily == AddressFamily.InterNetwork)
                        {
                            localIP = ip; //got it!
                            break;
                        }
                    }
                }
            }
            return localIP;
        }

        private void Initialize()
        {
            toObjectManager = new Channel<PhiInternalPackage>(100);
            Booster.Boost(new ObjectManager(toObjectManager));
        }

        private void doStart()
        {

            portmodifier = new Channel<int>(2);
            toBroadcaster = new Channel<BroadcastObject>(5);
            List<Boostable> processes = new List<Boostable>();

            processes.Add(new ConnectionManager(portmodifier));

            portmodifier.Send(bc.Port);

            if (broadcasting)
            {
                processes.Add(new Broadcaster(toBroadcaster, broadcastport, broadcastinterval));
                toBroadcaster.Send(bc);
            }
            Booster.Boost(processes);
            running = true;
        }

        internal Channel<PhiInternalPackage> ObjectManagerChannel
        {
            get
            {
                lock (mutex)
                {
                    return toObjectManager;
                }
            }
        }

        internal void StartConnect(string host, int port)
        {
            PeerWorker pc = null;
            lock (mutex)
            {
                if (!running && StorageManager.Instance.ReadyToRun &&
                    ReceiveBroadcast.isPortAvailable(broadcastport) &&
                    ReceiveBroadcast.isPortAvailable(bc.Port))
                {
                    Initialize();
                    //initialize connection to external server
                    pc = new PeerWorker();
                    if (pc.ConnectAndLaunch(host, port) == ReturnCode.Success)
                    {
                        //I could be a reconnect - let other servers check clear previous connections before launching self.
                        pc.Peer.PeerWorkerChannel.Send(new ForwardedPackage(new Gossip(), null)); 

                        StorageManager.Instance.ClearDevices();
                        PeerManager.Instance.AddPeer(pc.Peer);
                        doStart();
                        try
                        {
                            pc.Peer.PeerWorkerChannel.Send(
                                new ForwardedPackage(new AddPeer(PhiServer.Instance.BroadcastObject.IP, PhiServer.Instance.BroadcastObject.Port)
                                                   , null)
                                                   );
                        }
                        catch (PoisonException)
                        {

                        }
                    }
                    else
                    {
                        pc = null;
                    }
                }
            }
            if (pc != null) //connect was successful
            {
                //fetch peers from external server
                Channel<object> fromPeer = new Channel<object>(1);

                ForwardedPackage fa = new ForwardedPackage(new ListPeers(), fromPeer);
                pc.Peer.PeerWorkerChannel.Send(fa);

                object reply = fromPeer.Receive();
                if (reply is List<BroadcastObject>)
                {
                    List<BroadcastObject> peers = (List<BroadcastObject>)reply;

                    foreach (BroadcastObject p in peers)
                    {
                        if (!p.Equals(PhiServer.Instance.BroadcastObject))
                        {
                            Log.Instance.AddLogEntry("PhiServer","Connecting to peer :" + bc.ToString());
                            PeerWorker peerC = new PeerWorker();
                            ReturnCode rc = peerC.ConnectAndLaunch(p.IP, p.Port);
                            if (rc == ReturnCode.Success)
                            {
                                if (PeerManager.Instance.AddPeer(peerC.Peer))
                                {
                                    try
                                    {
                                        peerC.Peer.PeerWorkerChannel.Send(
                                        new ForwardedPackage(new AddPeer(PhiServer.Instance.BroadcastObject.IP, PhiServer.Instance.BroadcastObject.Port)
                                                           , null)
                                                           );
                                    }
                                    catch (PoisonException)
                                    {
                                        //connection is lost
                                    }

                                }
                                else
                                {
                                    peerC.Peer.PeerWorkerChannel.Poison();
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// updates local IP
        /// </summary>
        /// <param name="newIP">new ip-address</param>
        /// <returns>whether or not the ip was changed</returns>
        internal bool UpdateBroadcastObject(IPAddress newIP)
        {
            if (!IPAddress.IsLoopback(newIP) && myIP() != newIP && !newIP.Equals(PhiServer.Instance.BroadcastObject.IP))
            {
                Log.Instance.AddLogEntry("PhiServer", "Updated IP for Broadcast Object");
                this.bc = new BroadcastObject(bc.PublicKey, newIP, bc.Port);
                return true;
            }
            else
                return false;
        }
    }
}
